﻿// ----------------------------------------------------------------------------------
// Microsoft Developer & Platform Evangelism
// 
// Copyright (c) Microsoft Corporation. All rights reserved.
// 
// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 
// EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES 
// OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.
// ----------------------------------------------------------------------------------
// The example companies, organizations, products, domain names,
// e-mail addresses, logos, people, places, and events depicted
// herein are fictitious.  No association with any real company,
// organization, product, domain name, email address, logo, person,
// places, or events is intended or should be inferred.
// ----------------------------------------------------------------------------------

namespace Bowlus.Azure.ServiceManagementWrapper
{
    using System;
    using System.Globalization;
    using System.Net;
    using System.Runtime.Serialization;
    using System.Security.Cryptography.X509Certificates;
    using System.ServiceModel;
    using System.ServiceModel.Channels;
    using System.ServiceModel.Web;
    using System.Text;
    using System.Xml;
    using Bowlus.Azure.ServiceManagementWrapper.Contracts;

    public class ServiceManagementWrapper : Bowlus.Azure.ServiceManagementWrapper.IServiceManagementWrapper
    {
        private readonly string subscriptionId;
        private readonly X509Certificate2 managementCertificate;
        private readonly string versionHeaderContent;

        public ServiceManagementWrapper(string subscriptionId, X509Certificate2 managementCertificate)
            : this(subscriptionId, managementCertificate, Constants.DefaultVersionHeaderContent)
        {
        }

        public ServiceManagementWrapper(string subscriptionId, X509Certificate2 managementCertificate, string versionHeaderContent)
        {
            this.subscriptionId = subscriptionId;
            this.managementCertificate = managementCertificate;
            this.versionHeaderContent = versionHeaderContent;
        }

        public virtual AffinityGroupCollection ListAffinityGroups()
        {
            return this.CallOperation(proxy => proxy.EndListAffinityGroups(proxy.BeginListAffinityGroups(this.subscriptionId, null, null)));
        }

        public virtual AffinityGroup GetAffinityGroup(string affinityGroupName)
        {
            return this.CallOperation(proxy => proxy.EndGetAffinityGroup(proxy.BeginGetAffinityGroup(this.subscriptionId, affinityGroupName, null, null)));
        }

        public virtual CertificateCollection ListCertificates(string serviceName)
        {
            return this.CallOperation(proxy => proxy.EndListCertificates(proxy.BeginListCertificates(this.subscriptionId, serviceName, null, null)));
        }

        public virtual Certificate GetCertificate(string serviceName, string algorithm, string thumbprint)
        {
            return this.CallOperation(proxy => proxy.EndGetCertificate(proxy.BeginGetCertificate(this.subscriptionId, serviceName, algorithm, thumbprint, null, null)));
        }

        public virtual void AddCertificates(string serviceName, CertificateFile input)
        {
            this.CallOperation(proxy => proxy.EndAddCertificates(proxy.BeginAddCertificates(this.subscriptionId, serviceName, input, null, null)));
        }

        public virtual void DeleteCertificate(string serviceName, string algorithm, string thumbprint)
        {
            this.CallOperation(proxy => proxy.EndDeleteCertificate(proxy.BeginDeleteCertificate(this.subscriptionId, serviceName, algorithm, thumbprint, null, null)));
        }

        public virtual void SwapDeployment(string serviceName, SwapDeploymentInput input)
        {
            this.CallOperation(proxy => proxy.EndSwapDeployment(proxy.BeginSwapDeployment(this.subscriptionId, serviceName, input, null, null)));
        }

        public virtual void CreateOrUpdateDeployment(string serviceName, string deploymentSlot, CreateDeploymentInput input)
        {
            if (input == null)
            {
                throw new ArgumentNullException("input");
            }

            this.CallOperation(proxy => proxy.EndCreateOrUpdateDeployment(proxy.BeginCreateOrUpdateDeployment(this.subscriptionId, serviceName, deploymentSlot, input, null, null)));
        }

        public virtual void DeleteDeployment(string serviceName, string deploymentName)
        {
            this.CallOperation(proxy => proxy.EndDeleteDeployment(proxy.BeginDeleteDeployment(this.subscriptionId, serviceName, deploymentName, null, null)));
        }

        public virtual void DeleteDeploymentBySlot(string serviceName, string deploymentSlot)
        {
            this.CallOperation(proxy => proxy.EndDeleteDeploymentBySlot(proxy.BeginDeleteDeploymentBySlot(this.subscriptionId, serviceName, deploymentSlot, null, null)));
        }

        public virtual ServiceDeployment GetDeployment(string serviceName, string deploymentName)
        {
            var deployment = this.CallOperation(proxy => proxy.EndGetDeployment(proxy.BeginGetDeployment(this.subscriptionId, serviceName, deploymentName, null, null)));

            deployment.Label = DecodeFromBase64String(deployment.Label);
            deployment.Configuration = DecodeFromBase64String(deployment.Configuration);

            return deployment;
        }

        public virtual ServiceDeployment GetDeploymentBySlot(string serviceName, string deploymentSlot)
        {
            var deployment = this.CallOperation(proxy => proxy.EndGetDeploymentBySlot(proxy.BeginGetDeploymentBySlot(this.subscriptionId, serviceName, deploymentSlot, null, null)));

            deployment.Label = DecodeFromBase64String(deployment.Label);
            deployment.Configuration = DecodeFromBase64String(deployment.Configuration);

            return deployment;
        }

        public virtual void UpdateDeploymentStatus(string serviceName, string deploymentName, UpdateDeploymentStatusInput input)
        {
            if (input == null)
            {
                throw new ArgumentNullException("input");
            }

            this.CallOperation(proxy => proxy.EndUpdateDeploymentStatus(proxy.BeginUpdateDeploymentStatus(this.subscriptionId, serviceName, deploymentName, input, null, null)));
        }

        public virtual void UpdateDeploymentStatusBySlot(string serviceName, string deploymentSlot, UpdateDeploymentStatusInput input)
        {
            if (input == null)
            {
                throw new ArgumentNullException("input");
            }

            this.CallOperation(proxy => proxy.EndUpdateDeploymentStatusBySlot(proxy.BeginUpdateDeploymentStatusBySlot(this.subscriptionId, serviceName, deploymentSlot, input, null, null)));
        }

        public virtual void ChangeConfiguration(string serviceName, string deploymentName, ChangeConfigurationInput input)
        {
            if (input == null)
            {
                throw new ArgumentNullException("input");
            }

            input.Configuration = EncodeToBase64String(input.Configuration);
            this.CallOperation(proxy => proxy.EndChangeConfiguration(proxy.BeginChangeConfiguration(this.subscriptionId, serviceName, deploymentName, input, null, null)));
        }

        public virtual void ChangeConfigurationBySlot(string serviceName, string deploymentSlot, ChangeConfigurationInput input)
        {
            if (input == null)
            {
                throw new ArgumentNullException("input");
            }

            input.Configuration = EncodeToBase64String(input.Configuration);
            this.CallOperation(proxy => proxy.EndChangeConfigurationBySlot(proxy.BeginChangeConfigurationBySlot(this.subscriptionId, serviceName, deploymentSlot, input, null, null)));
        }

        public virtual void UpgradeDeployment(string serviceName, string deploymentName, UpgradeDeploymentInput input)
        {
            if (input == null)
            {
                throw new ArgumentNullException("input");
            }

            this.CallOperation(proxy => proxy.EndUpgradeDeployment(proxy.BeginUpgradeDeployment(this.subscriptionId, serviceName, deploymentName, input, null, null)));
        }

        public virtual void UpgradeDeploymentBySlot(string serviceName, string deploymentSlot, UpgradeDeploymentInput input)
        {
            if (input == null)
            {
                throw new ArgumentNullException("input");
            }

            this.CallOperation(proxy => proxy.EndUpgradeDeploymentBySlot(proxy.BeginUpgradeDeploymentBySlot(this.subscriptionId, serviceName, deploymentSlot, input, null, null)));
        }

        public virtual void WalkUpgradeDomain(string serviceName, string deploymentName, WalkUpgradeDomainInput input)
        {
            if (input == null)
            {
                throw new ArgumentNullException("input");
            }

            this.CallOperation(proxy => proxy.EndWalkUpgradeDomain(proxy.BeginWalkUpgradeDomain(this.subscriptionId, serviceName, deploymentName, input, null, null)));
        }

        public virtual void WalkUpgradeDomainBySlot(string serviceName, string deploymentSlot, WalkUpgradeDomainInput input)
        {
            if (input == null)
            {
                throw new ArgumentNullException("input");
            }

            this.CallOperation(proxy => proxy.EndWalkUpgradeDomainBySlot(proxy.BeginWalkUpgradeDomainBySlot(this.subscriptionId, serviceName, deploymentSlot, input, null, null)));
        }

        public virtual OperatingSystemCollection ListOperatingSystems()
        {
            return this.CallOperation(proxy => proxy.EndListOperatingSystems(proxy.BeginListOperatingSystems(this.subscriptionId, null, null)));
        }

        public virtual HostedServiceCollection ListHostedServices()
        {
            return this.CallOperation(proxy => proxy.EndListHostedServices(proxy.BeginListHostedServices(this.subscriptionId, null, null)));
        }

        public virtual HostedService GetHostedService(string serviceName)
        {
            try
            {
                return this.CallOperation(proxy => proxy.EndGetHostedService(proxy.BeginGetHostedService(this.subscriptionId, serviceName, null, null)));
            }
            catch (CommunicationException)
            {
                return null;
            }
        }

        public virtual HostedService GetHostedServiceWithDetails(string serviceName, bool embedDetail)
        {
            return this.CallOperation(proxy => proxy.EndGetHostedServiceWithDetails(proxy.BeginGetHostedServiceWithDetails(this.subscriptionId, serviceName, embedDetail, null, null)));
        }

        public virtual StorageServiceCollection ListStorageServices()
        {
            return this.CallOperation(proxy => proxy.EndListStorageServices(proxy.BeginListStorageServices(this.subscriptionId, null, null)));
        }

        public virtual StorageService GetStorageService(string name)
        {
            try
            {
                return this.CallOperation(proxy => proxy.EndGetStorageService(proxy.BeginGetStorageService(this.subscriptionId, name, null, null)));
            }
            catch (CommunicationException)
            {
                return null;
            }
        }

        public virtual StorageService GetStorageKeys(string name)
        {
            return this.CallOperation(proxy => proxy.EndGetStorageKeys(proxy.BeginGetStorageKeys(this.subscriptionId, name, null, null)));
        }

        public virtual StorageService RegenerateStorageServiceKeys(string name, RegenerateKeys regenerateKeys)
        {
            return this.CallOperation(proxy => proxy.EndRegenerateStorageServiceKeys(proxy.BeginRegenerateStorageServiceKeys(this.subscriptionId, name, regenerateKeys, null, null)));
        }

        public virtual Operation GetOperationStatus(string operationId)
        {
            return this.CallOperation(proxy => proxy.EndGetOperationStatus(proxy.BeginGetOperationStatus(this.subscriptionId, operationId, null, null)));
        }

        private static string GetExceptionDetails(Exception exception)
        {
            ServiceManagementError error = null;
            TryGetExceptionDetails(exception, out error);

            if (error == null)
            {
                return exception.ToString();
            }
            else
            {
                string errorDetails = string.Format(
                    CultureInfo.InvariantCulture,
                    "HTTP Status Code: {0} - HTTP Error Message: {1}",
                    error.Code,
                    error.Message);

                return errorDetails;
            }
        }

        private static bool TryGetExceptionDetails(Exception exception, out ServiceManagementError errorDetails)
        {
            HttpStatusCode httpStatusCode;
            string operationId;
            return TryGetExceptionDetails(exception, out errorDetails, out httpStatusCode, out operationId);
        }

        private static bool TryGetExceptionDetails(Exception exception, out ServiceManagementError errorDetails, out HttpStatusCode httpStatusCode, out string operationId)
        {
            errorDetails = null;
            httpStatusCode = 0;
            operationId = null;

            if (exception == null)
            {
                return false;
            }

            if (exception.Message == "Internal Server Error")
            {
                httpStatusCode = HttpStatusCode.InternalServerError;
                return true;
            }

            var wex = exception.InnerException as WebException;

            if (wex == null)
            {
                return false;
            }

            var response = wex.Response as HttpWebResponse;
            if (response == null)
            {
                return false;
            }

            httpStatusCode = response.StatusCode;
            if (httpStatusCode == HttpStatusCode.Forbidden)
            {
                return true;
            }

            if (response.Headers != null)
            {
                operationId = response.GetResponseHeader(Constants.OperationTrackingIdHeader);
            }

            using (var s = response.GetResponseStream())
            {
                if (s != null || s.Length == 0)
                {
                    return false;
                }

                try
                {
                    var reader = XmlDictionaryReader.CreateTextReader(s, new XmlDictionaryReaderQuotas());
                    var ser = new DataContractSerializer(typeof(ServiceManagementError));
                    errorDetails = (ServiceManagementError)ser.ReadObject(reader, true);
                }
                catch (SerializationException)
                {
                    return false;
                }
            }

            return true;
        }

        private static IServiceManagement CreateServiceManagementChannel(X509Certificate2 cert, string versionHeaderContent)
        {
            return CreateServiceManagementChannel(Constants.ServiceManagementWebHttpBinding, new Uri(Constants.ServiceManagementEndpoint), cert, versionHeaderContent);
        }

        private static IServiceManagement CreateServiceManagementChannel(Binding binding, Uri remoteUri, X509Certificate2 cert, string versionHeaderContent)
        {
            var factory = new WebChannelFactory<IServiceManagement>(binding, remoteUri);
            factory.Endpoint.Behaviors.Add(new ClientOutputMessageInspector(versionHeaderContent));
            factory.Credentials.ClientCertificate.Certificate = cert;
            return factory.CreateChannel();
        }

        private static string EncodeToBase64String(string original)
        {
            return Convert.ToBase64String(Encoding.UTF8.GetBytes(original));
        }

        private static string DecodeFromBase64String(string original)
        {
            return Encoding.UTF8.GetString(Convert.FromBase64String(original));
        }

        private void CallOperation(Action<IServiceManagement> call)
        {
            try
            {
                call(CreateServiceManagementChannel(this.managementCertificate, this.versionHeaderContent));
            }
            catch (CommunicationException ex)
            {
                var errorDetails = GetExceptionDetails(ex);
                throw new CommunicationException(errorDetails, ex);
            }
        }

        private TResult CallOperation<TResult>(Func<IServiceManagement, TResult> call)
        {
            try
            {
                return call(CreateServiceManagementChannel(this.managementCertificate, this.versionHeaderContent));
            }
            catch (CommunicationException ex)
            {
                var errorDetails = GetExceptionDetails(ex);
                throw new CommunicationException(errorDetails, ex);
            }
        }
    }
}